Tenant Extraction Refactoring Guide
Overview
This guide shows how to refactor existing API routes to use the centralized getTenantFromRequest utility from src/lib/tenant/tenant-extractor.ts.
Before: Manual Tenant Extraction (Old Pattern)
// ❌ OLD: Manual tenant extraction
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getDatabase } from '@/lib/database'
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) {
return new NextResponse('Unauthorized', { status: 401 })
}
const tenantId = (session.user as any).tenant_id
const db = getDatabase()
// Manual query - error-prone, inconsistent
const result = await db.query(`
SELECT id, name FROM agents
WHERE tenant_id = $1 OR tenant_id IS NULL
`, [tenantId])
return NextResponse.json(result.rows)
}After: Centralized Tenant Extraction (New Pattern)
// ✅ NEW: Centralized tenant extraction
import { NextRequest, NextResponse } from 'next/server'
import { getTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'
export async function GET(req: NextRequest) {
// Single function call handles all extraction methods
const tenant = await getTenantFromRequest(req)
if (!tenant) {
return NextResponse.json(
{ error: 'Tenant not found' },
{ status: 404 }
)
}
const db = getDatabase()
// Clean query with proper tenant isolation
const result = await db.query(`
SELECT id, name FROM agents
WHERE tenant_id = $1
`, [tenant.id])
return NextResponse.json(result.rows)
}With Required Tenant (Throws on Missing)
import { NextRequest, NextResponse } from 'next/server'
import { requireTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'
export async function POST(req: NextRequest) {
try {
// Throws automatically if tenant not found
const tenant = await requireTenantFromRequest(req)
const db = getDatabase()
const body = await req.json()
// ... rest of implementation
} catch (error: any) {
if (error.message.includes('Unauthorized')) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
if (error.message.includes('Tenant not found')) {
return NextResponse.json(
{ error: 'Tenant not found' },
{ status: 404 }
)
}
// Other errors
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Complete Refactoring Example: Agents Route
Before (`src/app/api/agents/route.ts`)
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getDatabase } from '@/lib/database'
import { TenantService } from '@/lib/tenant/tenant-service'
import { EmailService } from '@/lib/email'
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return new NextResponse('Unauthorized', { status: 401 })
}
const tenantId = (session.user as any).tenant_id
const db = getDatabase()
const result = await db.query(`
SELECT id, name, role FROM agent_registry
WHERE tenant_id = $1 OR tenant_id IS NULL
`, [tenantId])
return NextResponse.json(result.rows)
} catch (error) {
console.error('Failed to list agents:', error)
return new NextResponse('Internal Error', { status: 500 })
}
}After (Refactored)
import { NextRequest, NextResponse } from 'next/server'
import { getTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'
export async function GET(req: NextRequest) {
try {
// Centralized tenant extraction
const tenant = await getTenantFromRequest(req)
if (!tenant) {
return NextResponse.json(
{ error: 'Tenant not found' },
{ status: 404 }
)
}
const db = getDatabase()
const result = await db.query(`
SELECT id, name, role FROM agent_registry
WHERE tenant_id = $1
`, [tenant.id])
return NextResponse.json(result.rows)
} catch (error) {
console.error('Failed to list agents:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Benefits
1. **Consistency**
All routes use the same tenant extraction logic, reducing bugs.
2. **Multiple Extraction Methods**
Automatically tries multiple methods:
- Session (authenticated users)
- X-Tenant-ID header (internal calls)
- Subdomain (tenant.atom.ai)
- Custom domain (company.com)
3. **Proper Error Handling**
Centralized error handling with clear messages.
4. **Type Safety**
Full TypeScript support with TenantContext interface.
5. **Tenant Isolation**
No more OR tenant_id IS NULL bugs - always enforces tenant scoping.
Search & Replace Patterns
Find Manual Patterns to Replace
# Find manual session + tenant extraction
grep -r "getServerSession" src/app/api --include="*.ts"
grep -r "tenant_id = session" src/app/api --include="*.ts"
grep -r "tenant_id = (session.user" src/app/api --include="*.ts"
# Find direct database tenant queries
grep -r "SELECT.*tenant_id.*WHERE.*user_id" src/app/api --include="*.ts"Replacement Pattern
// BEFORE:
const session = await getServerSession(authOptions)
if (!session) {
return new NextResponse('Unauthorized', { status: 401 })
}
const tenantId = (session.user as any).tenant_id
// AFTER:
const tenant = await getTenantFromRequest(req)
if (!tenant) {
return NextResponse.json({ error: 'Tenant not found' }, { status: 404 })
}Testing
After refactoring, test these scenarios:
- **Authenticated user**: Tenant from session
- **Subdomain access**:
tenant.atom.ai-> subdomain extraction - **Custom domain**:
company.com-> custom domain lookup - **Internal API**:
X-Tenant-IDheader - **Missing tenant**: Proper 404 error
- **Unauthenticated**: Proper 401 error
Migration Checklist
- [ ] Update
src/app/api/agents/route.ts - [ ] Update
src/app/api/chat/route.ts - [ ] Update
src/app/api/sessions/route.ts - [ ] Update all integration routes
- [ ] Update workflow endpoints
- [ ] Run E2E tests
- [ ] Test tenant isolation
- [ ] Verify subdomain routing
- [ ] Test custom domain support
Files to Update
Based on the grep search, these files need refactoring:
src/app/api/desktop/auth/route.tssrc/app/api/agents/[id]/run/route.tssrc/app/api/chat/route.ts- Plus any other route with manual tenant extraction
Related Files
src/lib/tenant/tenant-extractor.ts- Centralized extraction logicbackend-saas/core/auth.py- Backend equivalent (get_current_tenant)src/lib/tenant/tenant-service.ts- Tenant business logic